mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-21 17:44:11 +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
|
|
@ -103,4 +103,63 @@ describe('useRum', () => {
|
|||
|
||||
expect(mockInit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initializes proxy RUM with the LibreChat bearer token for same-origin ingest', async () => {
|
||||
const fetchMock = jest.fn(() => Promise.resolve({}));
|
||||
window.fetch = fetchMock;
|
||||
mockUseGetStartupConfig.mockReturnValue({
|
||||
data: {
|
||||
rum: {
|
||||
provider: 'hyperdx',
|
||||
enabled: true,
|
||||
url: '/api/rum',
|
||||
serviceName: 'librechat-web',
|
||||
authMode: 'proxy',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderHook(() => useRum());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInit).toHaveBeenCalledWith({
|
||||
advancedNetworkCapture: false,
|
||||
apiKey: 'librechat-rum-proxy',
|
||||
consoleCapture: false,
|
||||
disableReplay: true,
|
||||
service: 'librechat-web',
|
||||
tracePropagationTargets: undefined,
|
||||
url: '/api/rum',
|
||||
});
|
||||
});
|
||||
|
||||
await window.fetch('/api/rum/v1/traces', { method: 'POST' });
|
||||
|
||||
const headers = fetchMock.mock.calls[0]?.[1]?.headers;
|
||||
expect(headers).toBeInstanceOf(Headers);
|
||||
expect((headers as Headers).get('authorization')).toBe('Bearer jwt-token');
|
||||
});
|
||||
|
||||
it('does not initialize proxy RUM without an authenticated token', async () => {
|
||||
mockUseAuthContext.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
token: undefined,
|
||||
user: undefined,
|
||||
});
|
||||
mockUseGetStartupConfig.mockReturnValue({
|
||||
data: {
|
||||
rum: {
|
||||
provider: 'hyperdx',
|
||||
enabled: true,
|
||||
url: '/api/rum',
|
||||
serviceName: 'librechat-web',
|
||||
authMode: 'proxy',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderHook(() => useRum());
|
||||
|
||||
expect(mockInit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ import { useGetStartupConfig } from '~/data-provider';
|
|||
import { useAuthContext } from '~/hooks/AuthContext';
|
||||
import { normalizeRumPath } from './routes';
|
||||
|
||||
const PROXY_API_KEY = 'librechat-rum-proxy';
|
||||
|
||||
let rumProxyToken: string | undefined;
|
||||
let rumProxyFetchPatched = false;
|
||||
|
||||
type HyperDXBrowser = {
|
||||
init: (config: {
|
||||
advancedNetworkCapture: boolean;
|
||||
|
|
@ -18,18 +23,70 @@ type HyperDXBrowser = {
|
|||
setGlobalAttributes: (attributes: Record<string, string>) => void;
|
||||
};
|
||||
|
||||
function shouldInitializeRum(config: TRumConfig | undefined): boolean {
|
||||
function shouldInitializeRum(config: TRumConfig | undefined, token: string | undefined): boolean {
|
||||
if (!config?.enabled || config.provider !== 'hyperdx' || !config.url || !config.serviceName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.authMode === 'publicToken' && !!config.publicToken;
|
||||
if (config.authMode === 'publicToken') {
|
||||
return !!config.publicToken;
|
||||
}
|
||||
|
||||
return config.authMode === 'proxy' && !!token && !config.publicToken;
|
||||
}
|
||||
|
||||
function getApiKey(config: TRumConfig): string {
|
||||
function getApiKey(config: TRumConfig, token: string | undefined): string {
|
||||
if (config.authMode === 'proxy') {
|
||||
return token ? PROXY_API_KEY : '';
|
||||
}
|
||||
|
||||
return config.publicToken ?? '';
|
||||
}
|
||||
|
||||
function isRumProxyRequest(input: RequestInfo | URL, proxyUrl: string): boolean {
|
||||
const rawUrl =
|
||||
typeof Request !== 'undefined' && input instanceof Request ? input.url : input.toString();
|
||||
const url = new URL(rawUrl, window.location.origin);
|
||||
const proxy = new URL(proxyUrl, window.location.origin);
|
||||
|
||||
return url.origin === window.location.origin && url.pathname.startsWith(`${proxy.pathname}/`);
|
||||
}
|
||||
|
||||
function withAuthorization(
|
||||
input: RequestInfo | URL,
|
||||
init: RequestInit | undefined,
|
||||
token: string,
|
||||
): [RequestInfo | URL, RequestInit | undefined] {
|
||||
const headers = new Headers(
|
||||
init?.headers ??
|
||||
(typeof Request !== 'undefined' && input instanceof Request ? input.headers : undefined),
|
||||
);
|
||||
headers.set('authorization', `Bearer ${token}`);
|
||||
|
||||
if (typeof Request !== 'undefined' && input instanceof Request) {
|
||||
return [new Request(input, { ...init, headers }), undefined];
|
||||
}
|
||||
|
||||
return [input, { ...init, headers }];
|
||||
}
|
||||
|
||||
function ensureRumProxyAuth(proxyUrl: string): void {
|
||||
if (rumProxyFetchPatched || typeof window === 'undefined' || typeof window.fetch !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (rumProxyToken && isRumProxyRequest(input, proxyUrl)) {
|
||||
const [authorizedInput, authorizedInit] = withAuthorization(input, init, rumProxyToken);
|
||||
return originalFetch(authorizedInput, authorizedInit);
|
||||
}
|
||||
|
||||
return originalFetch(input, init);
|
||||
};
|
||||
rumProxyFetchPatched = true;
|
||||
}
|
||||
|
||||
function buildGlobalAttributes(
|
||||
user: TUser | undefined,
|
||||
config: TRumConfig,
|
||||
|
|
@ -56,7 +113,7 @@ async function loadHyperDX(): Promise<HyperDXBrowser> {
|
|||
|
||||
export default function useRum(): void {
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const { user } = useAuthContext();
|
||||
const { token, user } = useAuthContext();
|
||||
const location = useLocation();
|
||||
const initializedKeyRef = useRef<string | undefined>(undefined);
|
||||
const sampledInitKeyRef = useRef<string | undefined>(undefined);
|
||||
|
|
@ -75,13 +132,21 @@ export default function useRum(): void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!shouldInitializeRum(rumConfig)) {
|
||||
if (!shouldInitializeRum(rumConfig, token)) {
|
||||
if (rumConfig?.authMode === 'proxy') {
|
||||
rumProxyToken = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const config = rumConfig;
|
||||
const apiKey = getApiKey(config, token);
|
||||
if (config.authMode === 'proxy') {
|
||||
rumProxyToken = token;
|
||||
ensureRumProxyAuth(config.url);
|
||||
}
|
||||
|
||||
const initKey = [config.url, config.serviceName, config.authMode, config.publicToken].join(':');
|
||||
const initKey = [config.url, config.serviceName, config.authMode, apiKey].join(':');
|
||||
|
||||
if (initializedKeyRef.current === initKey) {
|
||||
return;
|
||||
|
|
@ -107,7 +172,7 @@ export default function useRum(): void {
|
|||
|
||||
HyperDX.init({
|
||||
advancedNetworkCapture: config.advancedNetworkCapture ?? false,
|
||||
apiKey: getApiKey(config),
|
||||
apiKey,
|
||||
consoleCapture: config.consoleCapture ?? false,
|
||||
disableReplay: config.disableReplay ?? true,
|
||||
service: config.serviceName,
|
||||
|
|
@ -124,7 +189,7 @@ export default function useRum(): void {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rumConfig, user]);
|
||||
}, [rumConfig, token, user]);
|
||||
|
||||
useEffect(() => {
|
||||
hyperDxRef.current?.setGlobalAttributes(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue