mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
feat(analytics): forward x-tauri-version and x-core-version on every backend request (#2514)
This commit is contained in:
@@ -217,6 +217,10 @@ impl CoreProcessHandle {
|
||||
// the same env, matching what a child sidecar would have
|
||||
// received via Command::env.
|
||||
std::env::set_var("OPENHUMAN_CORE_TOKEN", self.rpc_token.as_str());
|
||||
// Surface the Tauri shell version to the in-process core so
|
||||
// backend-bound HTTP requests can attach `x-tauri-version`
|
||||
// analytics headers alongside `x-core-version`.
|
||||
std::env::set_var("OPENHUMAN_TAURI_VERSION", env!("CARGO_PKG_VERSION"));
|
||||
*self.active_port.write() = port;
|
||||
*self.last_port_fallback.write() = None;
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { getVersion } from '@tauri-apps/api/app';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { callCoreRpc } from '../coreRpcClient';
|
||||
|
||||
vi.mock('@tauri-apps/api/app', () => ({ getVersion: vi.fn() }));
|
||||
vi.mock('../coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
|
||||
|
||||
const configMock = vi.hoisted(() => ({ isDev: true }));
|
||||
|
||||
@@ -19,6 +22,7 @@ describe('apiClient version headers', () => {
|
||||
vi.clearAllMocks();
|
||||
configMock.isDev = true;
|
||||
vi.mocked(isTauri).mockReturnValue(false);
|
||||
vi.mocked(callCoreRpc).mockResolvedValue({ result: { version: '0.0.0-core' } });
|
||||
vi.stubGlobal('fetch', vi.fn());
|
||||
});
|
||||
|
||||
@@ -39,9 +43,10 @@ describe('apiClient version headers', () => {
|
||||
expect(headers).not.toHaveProperty('x-tauri-version');
|
||||
});
|
||||
|
||||
it('adds sanitized x-tauri-version on Tauri backend requests', async () => {
|
||||
it('adds sanitized x-tauri-version and x-core-version on Tauri backend requests', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true);
|
||||
vi.mocked(getVersion).mockResolvedValue(' 1.2.3 (desktop)+build!? ');
|
||||
vi.mocked(callCoreRpc).mockResolvedValue({ result: { version: ' 4.5.6 (core)+abc ' } });
|
||||
|
||||
const fetchMock = vi.mocked(fetch);
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
@@ -56,6 +61,7 @@ describe('apiClient version headers', () => {
|
||||
const requestInit = fetchMock.mock.calls[0][1] as RequestInit;
|
||||
const headers = requestInit.headers as Record<string, string>;
|
||||
expect(headers['x-tauri-version']).toBe('1.2.3desktop+build');
|
||||
expect(headers['x-core-version']).toBe('4.5.6core+abc');
|
||||
expect(headers).not.toHaveProperty('x-web-version');
|
||||
});
|
||||
|
||||
@@ -112,11 +118,15 @@ describe('apiClient version headers', () => {
|
||||
vi.mocked(getVersion)
|
||||
.mockRejectedValueOnce(new Error('transient failure'))
|
||||
.mockResolvedValueOnce('2.3.4');
|
||||
vi.mocked(callCoreRpc).mockResolvedValue({ result: { version: '4.5.6' } });
|
||||
|
||||
const { getClientVersionHeaders } = await import('../clientVersionHeaders');
|
||||
|
||||
await expect(getClientVersionHeaders()).resolves.toEqual({});
|
||||
await expect(getClientVersionHeaders()).resolves.toEqual({ 'x-tauri-version': '2.3.4' });
|
||||
await expect(getClientVersionHeaders()).resolves.toEqual({ 'x-core-version': '4.5.6' });
|
||||
await expect(getClientVersionHeaders()).resolves.toEqual({
|
||||
'x-tauri-version': '2.3.4',
|
||||
'x-core-version': '4.5.6',
|
||||
});
|
||||
expect(getVersion).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,12 @@ import { getVersion } from '@tauri-apps/api/app';
|
||||
|
||||
import { APP_VERSION } from '../utils/config';
|
||||
import { isTauri } from '../utils/tauriCommands/common';
|
||||
import { callCoreRpc } from './coreRpcClient';
|
||||
|
||||
const CLIENT_VERSION_MAX_LENGTH = 64;
|
||||
|
||||
let tauriVersionPromise: Promise<string | null> | null = null;
|
||||
let coreVersionPromise: Promise<string | null> | null = null;
|
||||
|
||||
export function sanitizeClientVersion(raw: string | null | undefined): string | null {
|
||||
const sanitized = String(raw ?? '')
|
||||
@@ -33,10 +35,37 @@ async function getTauriClientVersion(): Promise<string | null> {
|
||||
return tauriVersionPromise;
|
||||
}
|
||||
|
||||
async function getCoreClientVersion(): Promise<string | null> {
|
||||
if (!isTauri()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!coreVersionPromise) {
|
||||
coreVersionPromise = callCoreRpc<{ result?: { version?: string } }>({
|
||||
method: 'openhuman.update_version',
|
||||
params: {},
|
||||
})
|
||||
.then(response => sanitizeClientVersion(response?.result?.version))
|
||||
.catch(() => {
|
||||
// Clear the cached promise so a later call can retry once core is up.
|
||||
coreVersionPromise = null;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
return coreVersionPromise;
|
||||
}
|
||||
|
||||
export async function getClientVersionHeaders(): Promise<Record<string, string>> {
|
||||
if (isTauri()) {
|
||||
const tauriVersion = await getTauriClientVersion();
|
||||
return tauriVersion ? { 'x-tauri-version': tauriVersion } : {};
|
||||
const [tauriVersion, coreVersion] = await Promise.all([
|
||||
getTauriClientVersion(),
|
||||
getCoreClientVersion(),
|
||||
]);
|
||||
const headers: Record<string, string> = {};
|
||||
if (tauriVersion) headers['x-tauri-version'] = tauriVersion;
|
||||
if (coreVersion) headers['x-core-version'] = coreVersion;
|
||||
return headers;
|
||||
}
|
||||
|
||||
const webVersion = sanitizeClientVersion(APP_VERSION);
|
||||
|
||||
@@ -77,6 +77,17 @@ fn build_backend_reqwest_client() -> Result<Client> {
|
||||
HeaderValue::from_str(&version).context("invalid x-core-version header value")?,
|
||||
);
|
||||
}
|
||||
// The Tauri shell sets `OPENHUMAN_TAURI_VERSION` to its own package version
|
||||
// before spawning the in-process core, so backend analytics can attribute
|
||||
// core-originated requests to the desktop shell build that hosts them.
|
||||
if let Ok(raw) = std::env::var("OPENHUMAN_TAURI_VERSION") {
|
||||
if let Some(version) = sanitize_client_version(&raw) {
|
||||
default_headers.insert(
|
||||
HeaderName::from_static("x-tauri-version"),
|
||||
HeaderValue::from_str(&version).context("invalid x-tauri-version header value")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-appropriate TLS backend: Windows → schannel (honors the OS
|
||||
// cert store, required for corporate TLS-inspection proxies); macOS /
|
||||
|
||||
@@ -219,6 +219,31 @@ async fn backend_client_sends_x_core_version_on_auth_requests() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backend_client_sends_x_tauri_version_when_env_set() {
|
||||
// Serialize against any concurrent test that also touches this env var.
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
let _guard = ENV_LOCK.lock().unwrap();
|
||||
|
||||
std::env::set_var("OPENHUMAN_TAURI_VERSION", "9.8.7-shell+test");
|
||||
let (base_url, captured) = spawn_header_capture_server().await;
|
||||
let client = BackendOAuthClient::new(&base_url).unwrap();
|
||||
let url = client.url_for("/probe").unwrap();
|
||||
let response = client.raw_client().get(url).send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
std::env::remove_var("OPENHUMAN_TAURI_VERSION");
|
||||
|
||||
let headers = captured.take();
|
||||
let request_headers = headers.last().unwrap();
|
||||
let tauri_version = request_headers
|
||||
.get("x-tauri-version")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap();
|
||||
assert_eq!(tauri_version, "9.8.7-shell+test");
|
||||
// Core version still flows alongside the new tauri version header.
|
||||
assert!(request_headers.get("x-core-version").is_some());
|
||||
}
|
||||
|
||||
// Regression: OPENHUMAN-TAURI-8K / Sentry issue 7473650958.
|
||||
// When config.api_url is a full LLM completions URL (e.g. /v1/chat/completions),
|
||||
// Url::join used to produce wrong paths like /v1/chat/teams/me/usage instead of
|
||||
|
||||
Reference in New Issue
Block a user