feat(analytics): forward x-tauri-version and x-core-version on every backend request (#2514)

This commit is contained in:
Steven Enamakel
2026-05-22 22:46:55 -07:00
committed by GitHub
parent 67c943bd97
commit 9bae6d5563
5 changed files with 84 additions and 5 deletions
+4
View File
@@ -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;
+13 -3
View File
@@ -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);
});
});
+31 -2
View File
@@ -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);
+11
View File
@@ -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 /
+25
View File
@@ -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